其他

自己实现一个new,call,bind
new

new调用上面已经实现过,现在来复习一下,不写代码,过一下流程。new调用是在this绑定一节中出现的,this绑定的方式主要是默认绑定(指向window),显式绑定(call,apply,bind,以及各种js内置api),隐式绑定主要是指向当前调用对象上下文,以及new调用(创建新对象),new调用执行了以下操作

  • 1.创建一个新对象
  • 2.将新对象的__proto__指向函数的prototype
  • 3.执行函数,重新显式绑定this,将this更改为新对象
  • 4.如果函数本身有return,并且是一个对象,就返回自身对象,否则返回新创建的对象。
call

思考以下代码,如何才能让foo指向bar?

1
2
3
4
5
6
7
8
9
10
11
12
function foo () {
console.log(this.name)
}
var bar = {name:'John'}
foo.call(bar) // John
// 最简单的方式,其实是想办法将foo函数移动到bar对象上,也就是
var bar = {
name: 'John',
foo () {
console.log(this.name) // John
}
}

现在来实现一下

1
2
3
4
5
6
7
8
9
10
11
function call2 () {
const [ctx,...args] = arguments // 如果是apply,不需要解构
// 1.给当前对象挂载一个fn变量,值是this,this是什么?就是当前调用call函数的前面的那个函数上下文,这里是foo
ctx.fn = this // 相当于bar = {name:'John',fn:foo()}
// 2.执行,如果有返回值需要返回
let result = ctx.fn(...args) // 相当于bar.fn(原来的参数) // 如果是apply,不需要解构
// 3必须要删除这个fn函数,因为原来的没有
Reflect.deleteProperty(ctx,'fn')
return result
}

bind

bind函数也是用来修改this的,不过函数不会立即执行。

  • 1.获取传入的对象(arguments)和调用的函数(this)
  • 2.返回一个函数
  • 3.执行调用的函数,将this指向传入的对象,并且考虑有参数情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 其实和上面call实现有点类似,不过函数不会立即执行
    Function.prototype.bind2 = function () {
    // 同样先获取传入的对象以及参数
    // ctxObj:传入的bar对象,args:后面的参数,如果有
    const [ctxObj,...args] = arguments
    // 1.保存this
    let self = this // this指向bind的调用者,就是foo函数本身
    // 2.返回一个函数,因为不会立即执行
    return function () {
    // 3.再次获取arguments,因为返回的函数调用,也可以传递参数
    const [bindargs] = arguments
    // 执行函数并且指向传入的对象,后面的参数是需要将bind后面的参数和执行函数后面的参数拼接起来返回
    return self.apply(ctxObj,[...args.concat(bindargs)])
    }
    }
    Object.prototype.toString原理
    一般最常见使用它来判断对象属于js的哪种内置类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Object.prototype.toString.call([]) // "[object Array]"

    Object.prototype.toString.call({}) // "[object Object]"

    Object.prototype.toString.call(function(){}) // "[object Function]"

    Object.prototype.toString.call(new RegExp) // "[object RegExp]"

    Object.prototype.toString.call(new Date) // "[object Date]"

    Object.prototype.toString.call(new Date) // "[object Date]"

    Object.prototype.toString.call(new Error) // "[object Error]"

    ...

    思考第一个问题,Object.prototype.toString()Object.toString()区别在哪里?实际上,这还是原型链的问题,之前在函数和对象的关系介绍中,我们知道,js内置对象本质就是函数,Object也不例外,所以,当我们调用Object.toString()方法时候,实际上,是在调用Function中的toString()方法,因为Object继承于Function,调用toString()会被Function中的同名方法重写覆盖,Number,Array等等都有自己的toString()方法

Object.prototype.toString原理是什么?

我记得红宝书第三版讲的是获取的是[[Class]]属性的值,这是ES3的东西。es6之后发生了变化。

说到这个,就不得不提到一个es6新增的类型,Symbol()。
Symbol可以接收一个字符串作为参数,也可以不传,只要调用就会创建一个保证不会覆盖的属性,并且不可以使用new关键字来实例化

1
2
3
4
5
6
const name = Symbol()
const age = Symbol()
const gender = Symbol('man')
//重用Symbol
const job = Symbol.for('CS') // 创建一个Symbol
let other = Symbol.for('CS') //

Object.prototype.tostring原理:
ECMA原文地址

可以看到,实际上,除了null和undefined做了特殊处理,剩下的获取的是builtinTag的值,什么是builtinTag?就是内置标签。
为什么说到Symbol,就是因为通通过Symbol.toStringTag设置builtinTag的值,这也就是Object.prototype.tostring的原理

1
2
3
4
5
6
7
8
 let obj = {}
Object.defineProperty(obj, Symbol.toStringTag, {
get: function () {
return 'customerClass'
},
})

console.log(Object.prototype.toString.call(obj)) // "[object customerClass]"
instanceof原理

instanceof,可以用来判断一个对象是否属于某个构造函数,也可以用来判断一个实例是否属于它的父类型。

1
2
3
4
5
6
7
8
 function Foo () {

}
Foo.prototype.xx = ...

var a = new Foo()
a instanceof Foo // true
a instanceof Object // true

知道了上面prototype__proto__之间的关系以及new操作符做的事情,就很容易知道实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function instanceof (L,R) {
// 先获取右侧的函数的prototype
var right = R.prototype
// 获取左侧对象的__proto__
var left = L.__proto__
while (true) {
// 判断是否全等,因为new调用,会创建新对象,将__proto__指向函数的prototype
if (left === right) {
return true
}
// 已经查找到Object.prototype了,没找到返回false
if (left === null) {
return false
}
// 下次循环继续查找下一个__proto__
left = left.__proto__
}
}

instanceof,typeof,Object.prototype.toString区别
in 和 hasOwnProperty区别

思考以下代码,如何区分是当前的值返回了undefined还是这个值不存在?

1
2
3
4
5
6
7
var obj = {
a:undefined
}
obj.a // undefined
obj.b // undefined



可以通过inhasOwnProperty来判断
1
2
3
4
a in obj // true
b in obj // false
obj.hasOwnProperty("a"); // true
obj.hasOwnProperty("b"); // false

这两者的区别是in会查找原型链,而且检查的是属性名。hasOwnProperty只会查找对象自身

对象,包装对象,js内置对象
对象的声明方式
  • 字面量形式

    1
    2
    3
    4
    var obj = {
    key:value
    }

  • 构造函数/(调用)形式

    1
    2
    var obj = new Object()
    obj.key = value
  • Object.create()

    语法:Object.create(proto,[propertiesObject])

    创建一个新对象,新创建对象的__proto__就是传入的现有对象

proto:新创建对象的原型对象

propertiesObject:可选。要添加到新对象的可枚举(新添加的属性是其自身的属性,而不是其原型链上的属性)的属性,与Object.defineProperties第二个参数属性相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 var obj = Object.create({b:1}) 
obj.b // 1, obj的__proto__是{b:1}

var obj = Object.create(null)
obj // {},没有__proto__

var obj = Object.create({},{
a:{
value: 1,
writable: true
},
b:{
value: 2,
writable: true
}
})
console.log(obj.a) // 1
console.log(obj.b) // 2

字面量形式和构造调用方式没有什么太大区别,字面量形式可以一次声明多个,构造调用只能一个一个去赋值。另外最后的object.create()这种方式可以接收参数,创建出来的对象的__proto__就是传入的对象,如果是null,没有__proto__

js内置对象

js中的内置对象是对象的子类型,本质上是函数,通过new操作符调用

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
包装对象

什么是包装对象?比如以下代码

1
2
3
4
var str = 'i am a string'
str.length // 13
str.substring(0,1) // i

我们的str只是一个字面量,为什么可以向对象一样去调用length,substring等这些方法?实际上,如果我们想去像对象一样操作,就必须转换为对象,只是js引擎会自动去做这些事情,并不需要我们手动操作。之前也说过new调用会创建一个新对象。以下这几种都是包装对象

  • String

  • Number

  • Boolean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var str = 'i am a string'
    // 相当于
    var str = new String('i am a string')
    console.log(str) // String {0:i,1:'',2:'a',3:'m',length:13}

    var num = 100
    // 相当于
    var num = new Number(100)
    console.log(num) // Number {100}

    var boo = true
    // 相当于
    var boo = new Boolean(true)
    console.log(boo) // Boolean {true}
    对象查找和对象中的[[Get]],[[Put]]

    如下代码,当我们访问obj.a时候会发生什么?

    1
    2
    3
    4
    var obj = {
    a:1
    }
    obj.a // 1

    看上去似乎直接将访问的变量返回,但实际上实现了一个[[Get]]操作,类似[[Get]]()函数调用

  • 首先会在对象中查找是否有相同名称属性,找到就返回这个值,

  • 没有找到,就会查找[[ProtoType]](__proto__)

  • 如果还查找不到,就会返回undefined

    1
    2
    3
    4
    5
    6
    7
    8
    // 类似如下代码
    var obj = {
    get a () {
    return 1
    }
    }
    obj.a // 1

    另外一个就是[[Put]],也就是修改值发生的事情,大致如下

  • 属性是否设置了属性描述符?,存在setter就执行setter

  • 属性描述符的writable是否是false?

  • 如果都不是,就设置这个值

    如果是被proxy代理的情况下,这里的[[Get]][[Put]]并不适用

参考资料:

https://juejin.cn/post/6844903953981767688 JavaScript:V8编译过程

https://juejin.cn/post/6961349015346610184 Event Loop 和 JS
引擎、渲染引擎的关系
https://www.cnblogs.com/wangfupeng1988/p/3977924.html 深入理解javascript原型和闭包(完结)

https://blog.csdn.net/szengtal/article/details/78726143 词法环境:ECMA实现(一)— 简介

https://juejin.cn/post/6926831181681917959#heading-6 详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包

https://262.ecma-international.org ECMA原文地址